package core;

import java.util.Iterator;
import java.util.Observable;
import java.util.Observer;

import persistence.ContainerDTO;
import persistence.DataPersistor;
import persistence.ItemDTO;
import persistence.ProductDTO;

import core.exception.IllegalAddException;
import core.exception.IllegalEditException;
import core.exception.IllegalMoveException;
import core.exception.IllegalRemoveException;
import core.exception.ModelException;
import core.exception.NotFoundException;
import core.exception.SaveException;


/**
 * ProductFacade is the model facade for all operations on Products.  This class is a Singleton
 * and is Observable.
 * @author Royce
 *
 */
public class ProductFacade extends Observable {

	private static ProductFacade _instance;
	private Observer _controller;
	
	/**
	 * A method to access the singleton Instance of this ProductFacade
	 * @return the instance, creating it if necessary.
	 * 
	 * @pre none.
	 * @post returns a valid ProductFacade.
	 */
	public static ProductFacade instance() {
		if (_instance == null) {
			_instance = new ProductFacade();
		}
		return _instance;
	}
	
	/**
	 * For testing purposes, clear all data that has to do with products.
	 */
	public static void clearAllData(){
		setInstance(null);
		ProductManager.setInstance(null);
	}
	
	/**
	 * Testing only.
	 * @param o
	 */
	public void addControllerForTest(Observer o){
		_controller = o;
	}
	

	/**
	 * Sets the instance of this ProductFacade to the one given
	 * @param facade the ProductFacade instance to replace this one
	 */
	protected static void setInstance(ProductFacade facade) {
		_instance = facade;
	}
	
	/**
	 * Private constructor.  This method should never be called outside the ProductFacade class.
	 */
	private ProductFacade() {
		//just make sure the instance exists
		ProductManager.instance();
	}
	
	
	/**
	 * Determines if it is valid to add the given Product to the given Container. 
	 * @param product The Product to test for adding.
	 * @param container The Container to test for adding into.  If the Container is NULL, 
	 * then the Product will not be added to any container
	 * @return true if it is valid to add the Product into the given Container, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement.
	 */
	public boolean canAddProduct(Product product, Container container) {
		return product != null;
	}
	
	/**
	 * Adds the given Product to the given Container.
	 * @param product The Product to add.
	 * @param container The Container to add the given Product to.  If the Container 
	 * is NULL, then the Product will not be added to any Container.
	 * @throws IllegalAddException if it is invalid to add the given Product into the 
	 * given Container.
	 * 
	 * @pre the container either null or is not a RootStorageUnit and does not already contain 
	 * the product (but it may be elsewhere in the system)
	 * @post the container now contains the given product, or if the given container was null,
	 * the product is now in the system but not in a container.
	 */
	public void createProduct(Product product, Container container) throws IllegalAddException {
		ProductManager.instance().addProduct(product);
		
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ProductDTO pDTO= new ProductDTO(product);
			DataPersistor.instance().getProductDAO().addProduct(pDTO);
			
			if (container != null) {
				ProductManager.instance().addProductToContainer(product, container);
			}
			ContainerDTO cDTO = new ContainerDTO(container);
			DataPersistor.instance().getProductContainerDAO().addRelationship(pDTO, cDTO);
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} 
		catch (Exception e) {
			try {
				DataPersistor.instance().getTransactionManager().endTransaction(false);
			} 
			catch (Exception e1) {
				throw new IllegalAddException(e1.getMessage());
			}
			throw new IllegalAddException(e.getMessage());
		}
		
		
		Notification notify = new Notification(product, NotificationType.ProductAdded);
		notifyObservers(notify);
	}
	
	
	public boolean canAddProductToContainer(Product product, Container container){
		return ProductManager.instance().canAddProductToContainer(product, container);
	}
	
	/**
	 * Inserts the given Product into the given Container
	 * @param product The Product to add
	 * @param container The Container to add the Product to
	 * @throws IllegalAddException when it is illegal to add the given Product into 
	 * the given Container
	 */
	public void addProductToContainer(Product product, Container container) 
			throws IllegalAddException {
		assert(container != null);
		
		if (!ProductManager.instance().canAddProductToContainer(product, container)){
			throw new IllegalAddException("Can't add product to container");
		}
		
		Container oldInSU = container.getStorageUnit().getContainerForProduct(product);
		
		ProductManager.instance().addProductToContainer(product, container);
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ProductDTO pDTO= new ProductDTO(product);
			ContainerDTO cDTO = new ContainerDTO(container);
			DataPersistor.instance().getProductContainerDAO().addRelationship(pDTO, cDTO);
			Iterator<Item> iter = ItemManager.instance().itemsInProductAndContainer(container, 
					product);
			while(iter != null && iter.hasNext()){
				ItemDTO iDTO = new ItemDTO(iter.next());
				DataPersistor.instance().getItemDAO().updateItem(iDTO);
			}
			
			if (oldInSU != null && oldInSU != container){
				ContainerDTO oldContainer = new ContainerDTO(oldInSU);
				DataPersistor.instance().getProductContainerDAO().removeRelationship(pDTO, 
						oldContainer);
			}
			
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} 
		catch (Exception e) {
			try {
				DataPersistor.instance().getTransactionManager().endTransaction(false);
			} 
			catch (Exception e1) {
				throw new IllegalAddException(e1.getMessage());
			}
			throw new IllegalAddException(e.getMessage());
		}
		
		Notification notify = new Notification(product, NotificationType.ProductChanged);
		notifyObservers(notify);
	}
	
	public void addProductToContainerNoSave(Product p, Container c){
		ProductManager.instance().addProductToContainer(p, c);
	}
	
	/**
	 * Determines if it is valid to remove the given Product from the system.
	 * @param product The Product to test for removing.
	 * @param container The container to test for removing from.
	 * @return true if it is valid to remove the Product, false otherwise.
	 * 
	 * @pre none.
	 * @post see return statement.
	 */
	public boolean canRemoveProduct(Product product, Container container) {
		return ProductManager.instance().canRemoveProductFromContainer(product, container);
	}
	
	/**
	 * Removes the given Product from the system.
	 * @param product The Product to remove.
	 * @param container The Container to remove the product from
	 * @throws SaveException 
	 * @pre the product is in the system and is contained in the given container
	 * @post the product is removed from the container.  It may still exist in other containers.
	 */
	public void removeProduct(Product product, Container container) throws SaveException {
		if (container instanceof RootStorageUnit)
			removeProduct(product);
		else
		{
			ProductManager.instance().removeProductFromContainer(product, container);
			try
			{
				ProductDTO pDTO = new ProductDTO(product);
				ContainerDTO cDTO = new ContainerDTO(container);
				DataPersistor.instance().getTransactionManager().beginTransaction();
				DataPersistor.instance().getProductContainerDAO().removeRelationship(pDTO, cDTO);
				DataPersistor.instance().getTransactionManager().endTransaction(true);
			}
			catch (Exception e)
			{
				try {
					DataPersistor.instance().getTransactionManager().endTransaction(false);
				} 
				catch (Exception e1) {
					throw new SaveException(e1.getMessage());
				}
				throw new SaveException (e.getMessage());
			}
		}
		Notification notify = new Notification(product, NotificationType.ProductRemoved);
		notifyObservers(notify);
	}
	
	/**
	 * Remove the product from the system.
	 * @param The product to remove
	 * @throws Exception 
	 * @throws IllegalRemoveException if it is invalid to remove the given Product
	 * @throws NotFoundException if the given product is not found in the system
	 * 
	 * @pre the product is in the system
	 * @post the product is removed from the entire model
	 */
	public void removeProduct(Product product) throws SaveException{
		ProductManager.instance().removeProduct(product);
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ProductDTO pDTO= new ProductDTO(product);
			removeProductFromContainers(product);
			DataPersistor.instance().getProductDAO().removeProduct(pDTO);
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} 
		catch (SaveException e) {
			DataPersistor.instance().getTransactionManager().endTransaction(false);
			throw e;
		}
		Notification notify = new Notification(product, NotificationType.ProductRemoved);
		notifyObservers(notify);		
	}
	
	private void removeProductFromContainers(Product product) throws SaveException 
	{
		try {
			ProductDTO pDTO = new ProductDTO(product);
			Iterator<Container> iter = ProductManager.instance().getContainersForProduct(product);
			while (iter.hasNext())
			{
				ContainerDTO cDTO = new ContainerDTO(iter.next());
				DataPersistor.instance().getProductContainerDAO().removeRelationship(pDTO, cDTO);
			}
		} catch (NotFoundException e) {
			//do nothing
		}
		
	}

	/**
	 * Moves the given Product into the given Container.
	 * @param product The Product to move.
	 * @param fromContainer the Container to move from.
	 * @param targetContainer The target Container.
	 * @throws IllegalMoveException if it is invalid to move the Product into the given Container
	 * @throws NotFoundException if the Product was not found in the system.
	 * @throws IllegalAddException 
	 * 
	 * @pre product exists in fromContainer and not in targetContainer
	 * @post product is not in fromContainer and is in targetContainer
	 */
	public void moveProduct(Product product, Container fromContainer, Container targetContainer)
			throws IllegalMoveException, NotFoundException {
		
		// moving the product is the same as adding it
		if (!canAddProduct(product, targetContainer))
			throw new IllegalMoveException("Unable to move product to target container");
		
		try {
			ProductFacade.instance().addProductToContainer(product, targetContainer);
		} catch (IllegalAddException e) {
			throw new IllegalMoveException(e.getMessage());
		}
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} catch (SaveException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		Notification notify = new Notification(product, NotificationType.ProductChanged);
		notifyObservers(notify);
	}
	
	/**
	 * Determines if it is valid to edit the given Product
	 * @param before The Product as it exists now
	 * @param barcode The Barcode for the new Product
	 * @param description The description for the new Product
	 * @param size The size for the new Product
	 * @param shelfLife The shelf life for the new Product
	 * @param threeMonthSupply The 3 month supply for the new Product
	 * @return true if it is valid to edit the given Product, false otherwise.
	 * @deprecated use canEditProduct(Product, Product) instead
	 * 
	 * @pre none
	 * @post see return statement
	 */
	@Deprecated
	public boolean canEditProduct(Product before, Barcode barcode, String description, 
			Amount size, int shelfLife, int threeMonthSupply) {
		if (Product.canCreate(barcode, description, size, shelfLife, threeMonthSupply)) {
			Product test = new Product(barcode, description, size, shelfLife, threeMonthSupply);
			return before.canEdit(test);
		}
		
		return false;
	}
	
	public boolean canEditProduct(Product before, Product after) {
		return before.canEdit(after);
	}
	
	/**
	 * 
	 * @param tag
	 * @param barcode
	 * @param description
	 * @param sizeValue
	 * @param sizeUnit
	 * @param shelfLife
	 * @param supply
	 * @return
	 * 
	 * @deprecated use canEditProduct (Product, Product) instead
	 */
	@Deprecated
	public boolean canEditProduct(Object tag, String barcode, String description, String sizeValue, 
			String sizeUnit, int shelfLife, int supply) 
	{
		try
		{
			Product myProduct = (Product)tag;
			Barcode myBarcode = new Barcode(barcode, BarcodeType.PRODUCT);
			Amount amt = new Amount(sizeValue, sizeUnit);
			return canEditProduct(myProduct, myBarcode, description, amt, shelfLife, supply);
		}
		catch(Exception e)
		{
			return false;
		}
	}
	
	/**
	 * Edits the given Product
	 * @param before The Product as it exists now
	 * @param barcode The Barcode for the new Product
	 * @param description The description for the new Product
	 * @param size The size for the new Product
	 * @param shelfLife The shelf life for the new Product
	 * @param threeMonthSupply The 3 month supply for the new Product
	 * @throws IllegalEditException if it invalid to edit the given Product.
	 * @throws NotFoundException if the given Product is not found in the system.
	 * @deprecated use editProduct(Product, Product) instead
	 * 
	 * @pre before exists in the system at least once, after does not exist
	 * @post after now exists in all places before did, before does not exist
	 */
	@Deprecated
	public void editProduct(Product before, Barcode barcode, String description, 
			Amount size, int shelfLife, int threeMonthSupply)
			throws IllegalEditException, NotFoundException {
		Product after = new Product(barcode, description, size, shelfLife, threeMonthSupply);
		before.editProduct(after);
		Notification notify = new Notification(after, NotificationType.ProductChanged);
		notifyObservers(notify);
	}
	
	public void editProduct(Product before, Product after) 
			throws IllegalEditException, SaveException {
		before.editProduct(after);
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ProductDTO pDTO = new ProductDTO (after);
			DataPersistor.instance().getProductDAO().updateProduct(pDTO);
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} 
		catch (SaveException e) {
			e.printStackTrace();
			try {
				DataPersistor.instance().getTransactionManager().endTransaction(false);
			} 
			catch (Exception e1) {
				throw new SaveException(e1.getMessage());
			}
		}
		Notification notify = new Notification(after, NotificationType.ProductChanged);
		notifyObservers(notify);
	}
	
	/**
	 * 
	 * @param tag
	 * @param barcode
	 * @param description
	 * @param count
	 * @param size
	 * @param shelfLife
	 * @param supply
	 * @throws IllegalEditException
	 * @deprecated use editProduct(Product, Product) instead
	 */
	@Deprecated
	public void editProduct(Object tag, String barcode, String description, String count, 
			String size, int shelfLife, int supply) throws IllegalEditException 
	{
		try
		{
			Product myProduct = (Product)tag;
			Barcode myBarcode = new Barcode(barcode, BarcodeType.PRODUCT);
			Amount amt = new Amount(count, size);
			editProduct(myProduct, myBarcode, description, amt, shelfLife, supply);
		}
		catch(Exception e)
		{
			throw new IllegalEditException(e.getMessage());
		}
	}
	
	/**
	 * Returns a reference to a Product given a barcode
	 * @param barcode the Barcode of the product to look for
	 * @return the reference to the Product, or null if not found
	 */
	public Product getProduct(Barcode barcode) {
		return ProductManager.instance().getProduct(barcode);
	}
	
	/**
	 * Returns a reference to a Product given a barcode String
	 * @param barcode the String representation of the Barcode of the product to look for
	 * @return the reference to the Product, or null if not found
	 */
	public Product getProduct(String barcode)
	{
		Barcode myBarcode = new Barcode(barcode, BarcodeType.PRODUCT);
		return getProduct(myBarcode);
	}
	
	/**
	 * 
	 * @return a reference to this Facade's manager.  Used for testing purposes only.
	 */
	protected ProductManager get_productManager() {
		return ProductManager.instance();
	}
	
	/**
	 * Checks if the description is valid product description
	 * 
	 * @param desc the proposed description
	 * 
	 * @return returns true if the proposed description is valid, else returns false
	 */
	public boolean canSetDescription(String desc)
	{
		return Product.canSetDescription(desc);
	}
	
	/**
	 * Checks if the combo of sizeValue and unit are valid
	 * 
	 * @param sizeValue the number of the given units
	 * @param unit the unit of measurement
	 * 
	 * @return returns true if the combo of parameters is valid, else returns false
	 */
	public boolean canSetAmount(String sizeValue, String unit)
	{
		Amount amt;
		try {
			amt = new Amount(sizeValue, unit);
		}
		catch (IllegalArgumentException e) {
			return false;
		}
		return Product.canSetSize(amt);
	}

	/**
	 * Checks if the shelfLife is valid
	 * @param shelfLife the proposed shelfLife
	 * @return returns true if the shelfLife is valid, else returns false
	 */
	public boolean canSetShelfLife(int shelfLife)
	{
		return Product.canSetShelfLife(shelfLife);
	}

	/**
	 * Checks if the 3 month supply is valid
	 * @param supply the proposed 3 month supply
	 * @return returns true if the 3 month supply is valid, false otherwise
	 */
	public boolean canSet3MoSupply(int supply) 
	{
		return Product.canSetThreeMonthSupply(supply);
	}

	/**
	 * Checks if a product can be created with the given parameters
	 * 
	 * @param barcode the product's barcode
	 * @param description the product's description
	 * @param sizeValue the number of units per product
	 * @param sizeUnit the unit of the product
	 * @param shelfLife the shelfLife of the product
	 * @param supply the 3 month supply of the product
	 * 
	 * @return returns true if a product can be created with these parameters
	 * 
	 * @deprecated use Product.canCreate() instead
	 */
	@Deprecated
	public boolean canCreateProduct(String barcode, String description,
			String sizeValue, String sizeUnit, int shelfLife, int supply) 
	{
		try
		{
			Barcode myBarcode = new Barcode(barcode, BarcodeType.PRODUCT);
			Amount amt = new Amount (sizeValue, sizeUnit);
			return Product.canCreate(myBarcode, description, amt, shelfLife, supply);
		}
		catch (Exception e)
		{
			return false;
		}
	}
	
	/**
	 * Adds a Product to the system.
	 * @param newProduct The product to add.
	 * @throws ModelException if any exception occurred in the model when adding the Product.
	 */
	public void addProduct(Product newProduct) throws ModelException {
		ProductManager.instance().addProduct(newProduct);
		
		try {
			DataPersistor.instance().getTransactionManager().beginTransaction();
			ProductDTO pDTO= new ProductDTO(newProduct);
			DataPersistor.instance().getProductDAO().addProduct(pDTO);
		
			DataPersistor.instance().getTransactionManager().endTransaction(true);
		} 
		catch (Exception e) {
			try {
				DataPersistor.instance().getTransactionManager().endTransaction(false);
			} 
			catch (Exception e1) {
				throw new IllegalAddException(e1.getMessage());
			}
			throw new IllegalAddException(e.getMessage());
		}
		
		Notification notify = new Notification(newProduct, NotificationType.ProductAdded);
		notifyObservers(notify);
	}
	
	public void loadProduct(Product oldProduct) throws ModelException{
		ProductManager.instance().addProduct(oldProduct);
	}
	
	@Override
	public void notifyObservers(Object arg) {
		if (_controller == null){
			this.setChanged();
			super.notifyObservers(arg);
		}
		else
			_controller.update(this, arg);
	}

	/**
	 * Gets the products for a given container
	 * 
	 * @param container the container
	 * 
	 * @return an iterator for the products
	 * 
	 * @throws NotFoundException thrown when no products in the container
	 */
	public Iterator<Product> getProductsForContainer(Container container) throws NotFoundException 
	{
		return ProductManager.instance().getProductsForContainer(container);
	}
	
	/**
	 * 
	 * @return an Iterator over all Products in the system.
	 */
	public Iterator<Product> getAllProducts() {
		return ProductManager.instance().getAllProducts();
	}
}
